﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Reflection;
using CodePlex.Diagnostics.Contracts.Conditions;

namespace CodePlex.Diagnostics.Contracts
{
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Property)]
  public class ContractValidationAttribute : Attribute
  {
    public static void Validate(object value, bool inherit, bool recursive)
    {
      Contract.Requires(!value.IsNull());

      Type valueType = value.GetType();
      Dictionary<string, PropertyInfo> properties = new Dictionary<string, PropertyInfo>();

      if (!GetProperties(properties, valueType, inherit) && inherit)
        foreach (Type interfaceType in valueType.GetInterfaces()) // Makes us inherit interface attributes.
        {
          Contract.Assume(!interfaceType.IsNull());
          GetProperties(properties, interfaceType, inherit);
        }

      foreach (PropertyInfo property in properties.Values)
      {
        object propertyValue = null;

        try
        {
          propertyValue = property.GetValue(value, null);
        }
        catch (Exception exception)
        {
          throw new ContractValidationException(property, exception);
        }

        // Validate returned objects if recursive.
        if (recursive &&
            (property.PropertyType.IsClass || property.PropertyType.IsInterface || property.PropertyType.IsLayoutSequential) &&
            propertyValue != null)
          Validate(propertyValue, inherit, recursive);
      }
    }

    public static bool IsValid(object value, bool inherit, bool recursive)
    {
      Contract.Requires(!value.IsNull());

      try
      {
        Validate(value, inherit, recursive);
      }
      catch // Only works if the contract runtime throws an exception.
      {
        return false;
      }
      return true;
    }

    private static bool GetProperties(Dictionary<string, PropertyInfo> properties, Type type, bool inherit)
    {
      Contract.Requires(!properties.IsNull());
      Contract.Requires(!type.IsNull());

      bool validateAll = type.IsDefined(typeof(ContractValidationAttribute), inherit);

      PropertyInfo[] typeProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
      foreach (PropertyInfo property in typeProperties)
        if (property.CanRead &&
            (validateAll || property.IsDefined(typeof(ContractValidationAttribute), inherit)) &&
            !properties.ContainsKey(property.Name))
          properties[property.Name] = property;

      return validateAll;
    }
  }
}
